/********************************************************************************
* @author: Ternura_xiang
* @email: 2022101883@nit.edu.cn
* @date: Apr 4, 2025
* @version: 1.0
* @description: ST7789屏幕驱动代码
********************************************************************************/

#include "st7789.h"
#include "font.h"
#include <math.h>
#include <stdlib.h>

/*
 * 一点RAM来加快速度。这使 HAL SPI 库可以批量传输数据，而不是单独的事务。
 * 最小值为 3（R、G 和 B 各 1 个字节）。
 * 理论最大值为 0xFFFF - 1，但这似乎行不通。
 * 选择适合的 RAM 预算并与控制器配合使用的大小。
 */
#define V_BUFFER_SIZE 0x3FFF
uint8_t v_buffer[V_BUFFER_SIZE];
uint16_t buffer_counter = 0;


/**
 * @brief 将 V-RAM 缓冲区写入显示器。
 * TODO: currently only uses SPI.
 */
void write_buffer() {
    HAL_SPI_Transmit(&LCD_SPI, (uint8_t *) &v_buffer, buffer_counter, 10);
    buffer_counter = 0;
}

/**
 * @brief 重启LCD
 * @retval None
 */
void LCD_Reset()
{
    LCD_RES_RESET;
    LCD_Delay(50);
    LCD_RES_SET;
    LCD_Delay(50);
}

/**
 * @brief 发送命令
 * @note 使用软件SPI时需重写此函数
 * @param command: 需要发送的指令
 * @retval None
 */
void LCD_WriteCommand(uint8_t command)
{
    // DC拉低，命令模式
    LCD_DC_RESET;
    // CS拉低，片选
    LCD_CS_RESET;
    LCD_SPI_Transmit(&LCD_SPI, &command, 1, 1000);
    LCD_CS_SET;
}

/**
 * @brief 发送数据
 * @note 使用软件SPI时需重写此函数
 * @param data: 需要发送的数据
 * @retval None
 */
void LCD_WriteData(uint8_t data)
{
    // DC拉低，数据模式
    LCD_DC_SET;
    // CS拉低，片选
    LCD_CS_RESET;
    LCD_SPI_Transmit(&LCD_SPI, &data, 1, 1000);
    LCD_CS_SET;
}

/**
 * @brief 发送16bit数据
 * @note 使用软件SPI时需重写此函数
 * @param data: 需要发送的数据
 * @retval None
 */
void LCD_WriteData_u16(uint16_t data)
{
    LCD_WriteData(data >> 8);
    LCD_WriteData(data & 0x0F);
}

/**
 * @brief 交换两个8bit数据
 */
void swap_8bit(char *num1, char *num2) {
    char temp = *num2;
    *num2 = *num1;
    *num1 = temp;
}

/**
 * @brief 交换两个16bit数据
 */
void swap_16bit(uint16_t *num1, uint16_t *num2) {
    int temp = *num2;
    *num2 = *num1;
    *num1 = temp;
}


/**
 * @brief LCD初始化
 * @retval None
 */
void LCD_Init()
{
    LCD_Reset();
    
    LCD_WriteCommand(LCD_SLPOUT);
    LCD_Delay(10);
    LCD_WriteCommand(LCD_MADCTL);
    LCD_WriteData((LCD_MY<<7)|(LCD_MX<<6)|(LCD_MV<<5)|(LCD_ML<<4)|(LCD_BGR<<3)|(LCD_MH<<2));
    LCD_WriteCommand(LCD_COLMOD);
    LCD_WriteData(0x05);
    LCD_WriteCommand(LCD_FRMCTR2);
    LCD_WriteData(0x0C);
    LCD_WriteData(0x0C);
    LCD_WriteData(0x00);
    LCD_WriteData(0x33);
    LCD_WriteData(0x33);
    LCD_WriteCommand(LCD_ETMOD);
    LCD_WriteData(0x35);
    LCD_WriteCommand(0xBB);
    LCD_WriteData(0x29);
    LCD_WriteCommand(LCD_PWRCTRL3);
    LCD_WriteData(0x01);
    LCD_WriteCommand(LCD_PWRCTRL4);
    LCD_WriteData(0x19);
    LCD_WriteCommand(LCD_PWRCTRL5);
    LCD_WriteData(0x20);
    LCD_WriteCommand(LCD_VCOMCTRL);
    LCD_WriteData(0x1A);
    LCD_WriteCommand(LCD_CABCCTRL1);
    LCD_WriteData(0x0F);
    LCD_WriteCommand(LCD_NVMWR);
    LCD_WriteData(0xA4);
    LCD_WriteData(0xA1);
    LCD_WriteCommand(LCD_PGAMCTRL);
    LCD_WriteData(0xD0);
	LCD_WriteData(0x08);
	LCD_WriteData(0x0E);
	LCD_WriteData(0x09);
	LCD_WriteData(0x09);
	LCD_WriteData(0x05);
	LCD_WriteData(0x31);
	LCD_WriteData(0x33);
	LCD_WriteData(0x48);
	LCD_WriteData(0x17);
	LCD_WriteData(0x14);
	LCD_WriteData(0x15);
	LCD_WriteData(0x31);
	LCD_WriteData(0x34);
    LCD_WriteCommand(LCD_NGAMCTRL);
    LCD_WriteData(0xD0);
	LCD_WriteData(0x08);
	LCD_WriteData(0x0E);
	LCD_WriteData(0x09);
	LCD_WriteData(0x09);
	LCD_WriteData(0x15);
	LCD_WriteData(0x31);
	LCD_WriteData(0x33);
	LCD_WriteData(0x48);
	LCD_WriteData(0x17);
	LCD_WriteData(0x14);
	LCD_WriteData(0x15);
	LCD_WriteData(0x31);
	LCD_WriteData(0x34);
    LCD_WriteCommand(LCD_INVON);
    LCD_WriteCommand(LCD_SLPOUT);
    LCD_Delay(120);
    LCD_WriteCommand(LCD_DISON);
}

/**
 * @brief 写入屏幕地址函数
 * @param x_start:  起始x坐标
 * @param y_start:  起始y坐标
 * @param x_end:    终点x坐标
 * @param y_end:    终点y坐标
 * @retval None
 */
void LCD_SetWindow(uint16_t x_start,uint16_t y_start,uint16_t x_end,uint16_t y_end)
{
    // 检查值是否按顺序排列
    if(x_end < x_start)
        swap_16bit(&x_end, &x_start);
    if(y_end < y_start)
        swap_16bit(&y_end, &y_start);

    LCD_WriteCommand(LCD_CASET);
    LCD_WriteData(x_start >> 8);
    LCD_WriteData(x_start & 0x00FF);

    LCD_WriteData(x_end >> 8);
    LCD_WriteData(x_end & 0x00FF);

    LCD_WriteCommand(LCD_PASET);
    LCD_WriteData(y_start >> 8);
    LCD_WriteData(y_start & 0x00FF);

    LCD_WriteData(y_end >> 8);
    LCD_WriteData(y_end & 0x00FF);

    LCD_WriteCommand(LCD_RAMWR);
}

/**
 * @brief 将RGB(R,G,B)转化为RGB565
 * @param red:      红色
 * @param green:    绿色
 * @param blue:     蓝色
 * @retval 转化后的16位RGB565
 */
uint16_t RGB565(uint8_t red, uint8_t green, uint8_t blue)
{
    uint16_t color = 0;
    color |= (red >> 3) << 11;      //将red的高5位放到color的高5位
    color |= (green >> 2) << 5;     //将green的高6位放到color的中间6位
    color |= blue >> 3;             //将blue的高5位放到color的低5位
    return color;
}

/**
 * @brief 区域颜色填充
 * @param x_start:  起始x坐标
 * @param y_start:  起始y坐标
 * @param x_end:    终点x坐标
 * @param y_end:    终点y坐标
 * @param color:    填充颜色
 * @retval None
 */
void LCD_FullArea(uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, uint16_t color)
{
    // 设置绘图区域
    LCD_SetWindow(x_start, y_start, x_end, y_end);

    // 为了提高速度，我们将在此处手动执行 SPI 写入
    // 数据表说 CS 在数据部分之间是否发生变化并不重要，但我不相信。
    // CS 低开始数据
    LCD_DC_SET;
    LCD_CS_RESET;

    uint8_t high_byte = color >> 8;
    uint8_t low_byte = color & 0xFF;

    for (int y = y_start; y <= y_end; y++) {
        for (int x = x_start; x <= x_end; x++) {
            v_buffer[buffer_counter++] = high_byte;
            v_buffer[buffer_counter++] = low_byte;

            if (buffer_counter >= V_BUFFER_SIZE - 2) {
                write_buffer();
            }
        }
    }
    // 发送剩余的字节
    write_buffer();
    // 将 CS 恢复到高
    LCD_CS_SET;
}

/**
 * @brief 填充整块屏幕
 * @param color:    填充的颜色
 * @retval None
 */
void LCD_Full(uint16_t color)
{
    LCD_FullArea(0, 0, LCD_HEIGHT, LCD_WIDTH, color);
}

/**
 * @brief 画点
 * @param x:        x坐标
 * @param y:        y坐标
 * @param color:    颜色
 * @retval None
 */
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color)
{
    LCD_SetWindow(x, y, x+1, y+1);
    LCD_WriteData(color >> 8);
    LCD_WriteData(color & 0xFF);
}

/**
 * @brief 画直线
 * @param x_start:  起始x坐标
 * @param y_start:  起始y坐标
 * @param x_end:    终点x坐标
 * @param y_end:    终点y坐标
 * @param width:    线宽
 * @param color:    颜色
 * @retval None
 */
 void LCD_DrawLine(uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, \
    uint16_t width, uint16_t color)
{
    int16_t x_length = x_end - x_start;
    int16_t y_length = y_end - y_start;
    int16_t dx = (x_length >= 0) ? 1 : -1;
    int16_t dy = (y_length >= 0) ? 1 : -1;
    x_length = abs(x_length);
    y_length = abs(y_length);

    if(x_length >= y_length)
    {
        // 水平主导线，垂直方向扩展线宽
        int16_t y = 0;
        float y_true, y_error;
        for (int16_t i = 0; i <= x_length; i++)
        {
            y_true = (i * y_length) / (float)x_length;
            y_error = y_true - y;
            if(y_error >= 0.5) y++;
            else if(y_error <= -0.5) y--;

            // 计算垂直扩展区域（对称）
            uint16_t x_pos = x_start + i * dx;
            uint16_t y_center = y_start + y * dy;
            uint16_t y_start_area = y_center - (width / 2);
            uint16_t y_end_area = y_start_area + width - 1;

            LCD_FullArea(x_pos, y_start_area, x_pos, y_end_area, color);
        }
    }
    else
    {
        // 垂直主导线，水平方向扩展线宽
        int16_t x = 0;
        float x_true, x_error;
        for (int16_t i = 0; i <= y_length; i++)
        {
            x_true = (i * x_length) / (float)y_length;
            x_error = x_true - x;
            if(x_error >= 0.5) x++;
            else if(x_error <= -0.5) x--;

            // 计算水平扩展区域（对称）
            uint16_t y_pos = y_start + i * dy;
            uint16_t x_center = x_start + x * dx;
            uint16_t x_start_area = x_center - (width / 2);
            uint16_t x_end_area = x_start_area + width - 1;

            LCD_FullArea(x_start_area, y_pos, x_end_area, y_pos, color);
        }
    }
}

/**
 * 画一个指定大小的字符()
 * @param x:    x坐标
 * @param y:    y坐标
 * @param c:    字符(支持ASCII 32-126)
 * @param color: 颜色
 * @param size: 字符的大小(倍)
 */
void LCD_DrawChar(uint16_t x, uint16_t y, char c, uint16_t color, uint8_t size)
{
    char line;
    unsigned int font_index = (c - 32);

    // 从字体文件中获取像素行。
    for(uint16_t i=0; i<13; i++ ) {
        line = ASCII[font_index][12 - i];

        // 绘制像素到屏幕
        for(uint16_t j=0; j<8; j++) {
            if(line & (0x01 << j)) {
                LCD_FullArea(x+((8-j)*size), y+((i)*size), x+((8-j)*size)+size, y+((i)*size)+size, color);
            }
        }
    }
}

/**
 * 显示字符串
 * @param x:    x坐标
 * @param y:    y坐标
 * @param color: 颜色
 * @param size: 字符的大小(倍)
 * @param str:  字符串
 */
void LCD_DrawStr(uint16_t x, uint16_t y, char *str, uint16_t color, uint8_t size)
{
    int char_width = size * 9;  // 计算出每个字符的大小

    // 遍历字符串中的每个字符
    int counter = 0;
    while(str[counter] != '\0') {
        int char_pos = x + (counter * char_width);  // 计算字符位置
        LCD_DrawChar(char_pos, y, str[counter], color, size);   // 将字符写入显示
        counter++;  // 下一个字符
    }
}

/**
 * 画直方图
 * @param x_start:      图表区域起始x坐标
 * @param y_start:      图表区域起始y坐标
 * @param x_length:     图表区域的x轴长度（像素）
 * @param y_length:     图表区域的y轴高度（像素）
 * @param background_color: 图表背景颜色
 * @param data:         数据数组指针
 * @param data_length:  数据长度
 * @param data_max:     数据最大值
 * @param data_color:   数据颜色
 * @param axis_width:   坐标轴线宽
 * @param axis_color:   坐标轴颜色
 * @return None
 * @note 本函数传入数据应为正整数，且不应大于25535。
 * 另本函数计划用于数据量很大的场景，所以并未设置每个数据之间的间隔。
 * 由于函数内存在全局静态数组，当x_length>320时，需要更改show_list的长度。
 */
void LCD_DrawHistogram(uint16_t x_start, uint16_t y_start, uint16_t x_length, uint16_t y_length,
    uint16_t background_color, uint16_t *data, uint16_t data_length, uint16_t data_max, 
    uint16_t data_color, uint16_t axis_width, uint16_t axis_color)
{
    // 计算常量
    uint16_t y_end = y_start + y_length;
    uint16_t x_end = x_start + x_length;
    float y_data = (float)y_length / (float)data_max;   // 影射坐标需要的量
    float x_data = (float)data_length / (float)x_length;
    uint8_t background_high_byte = background_color >> 8;   // 提前计算颜色值
    uint8_t background_low_byte = background_color & 0xFF;
    uint8_t data_high_byte = data_color >> 8;
    uint8_t data_low_byte = data_color & 0xFF;

    // 映射数据到像素
    static uint16_t show_list[320];   // 新的数据显示列表(用来将数据映射到显示像素)
    for (uint16_t i = 0; i < x_length; i++){
        show_list[i] = data[(uint16_t)(x_data * i)] * y_data + y_start;
    }

    // 显示
    LCD_SetWindow(x_start, y_start-1, x_end-1, y_end);
    LCD_DC_SET;
    LCD_CS_RESET;

    // 注：这里使用条件判断计算像素，随后一并发出。
    // 另外方案可选择先填充背景，然后填充柱子。个人觉得柱子不多时这种更快，所以没写。
    for (uint16_t y = y_end; y >= y_start; y--){
        for(uint16_t dx = 0; dx < x_length; dx++){
            if(show_list[dx] <= y){
                v_buffer[buffer_counter++] = background_high_byte;
                v_buffer[buffer_counter++] = background_low_byte;
            }
            else{
                v_buffer[buffer_counter++] = data_high_byte;
                v_buffer[buffer_counter++] = data_low_byte;
            }

            if(buffer_counter >= V_BUFFER_SIZE - 2){
                write_buffer();
            }
        }
    }
    write_buffer();
    LCD_CS_SET;

    // 画坐标轴
    LCD_DrawLine(x_start, y_end, x_end, y_end, axis_width, axis_color);
    LCD_DrawLine(x_start, y_start, x_start, y_end, axis_width, axis_color);
}
